// Copyright (c) 2007, Luke Stratman
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, 
// are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright notice, 
// this list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice, this 
// list of conditions and the following disclaimer in the documentation and/or other 
// materials provided with the distribution.
//
// * Neither the name of Luke Stratman nor the names of its contributors may be used 
// to endorse or promote products derived from this software without specific prior 
// written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
// IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
// BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 
// OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 
// OF THE POSSIBILITY OF SUCH DAMAGE.
//
/////////////////////////////////////////////////////////////////////////////////////

using System;
using System.Collections.Generic;
using System.Net;
using System.Text.RegularExpressions;
using System.Web;
using System.Xml;
using Rodunu.Business.Models.Movies;
using Rodunu.Services.NetFlix.Json;
using Stratman.Utilities;

namespace Rodunu.Services.NetFlix
{
    /// <summary>
    /// Encapsulates a series of helper methods that are designed to retrieve data from NetFlix's
    /// site via JSON and traditional HTTP GET and POST calls.
    /// </summary>
    public class NetFlixSession : IDisposable
    {
        private const int MAX_TO_SEARCH = 25;

        public enum QueueResult
        {
            Success,
            AlreadyInQueue,
            Failed
        }

        /// <summary>
        /// List of cookies that we pass to NetFlix during each HTTP call in order to identify the
        /// current user.
        /// </summary>
        protected CookieCollection cookies = null;
        /// <summary>
        /// Identification string for the current user to use when accessing their RSS feeds.
        /// </summary>
        protected string rssID = "";

        /// <summary>
        /// Logs in the user using the provided credentials, getting the list of cookies to use as
        /// well as the user's RSS ID.
        /// </summary>
        /// <param name="email">
        /// Email that was used to register the user with NetFlix.
        /// </param>
        /// <param name="password">
        /// User's NetFlix password.
        /// </param>
        public NetFlixSession(string email, string password)
        {
            Dictionary<string, string> postFields = new Dictionary<string, string>();

            // Prepare the POST data that we will use for the login attempt
            postFields.Add("email", email);
            postFields.Add("password1", password);
            postFields.Add("movieid", "");
            postFields.Add("trkid", "");
            postFields.Add("SubmitButton", "Click Here to Continue");
            postFields.Add("RememberMe", "True");
            postFields.Add("nextpage", "http://www.netflix.com/");

            // We do a GET request against the login page first, to get an initial collection of
            // cookies
            HttpWebResponse webResponse = WebRequestUtilities.MakeHttpGetRequest("http://www.netflix.com/Login");

            cookies = webResponse.Cookies;
            webResponse.Close();

            // Next, we do the actual login POST request
            webResponse = WebRequestUtilities.MakeHttpPostRequest("https://www.netflix.com/Login", postFields, cookies);

            cookies.Add(webResponse.Cookies);
            webResponse.Close();

            // Do a request against the member homepage to make sure that the user was logged in
            // successfully
            string responseText = WebRequestUtilities.MakeHttpGetRequestTextOnly("http://www.netflix.com/MemberHome", cookies);

            if (!responseText.Contains("<a href=\"http://www.netflix.com/YourAccount\">Your Account</a>"))
                throw new LoginFailedException(email);

            // Get the user's RSS ID
            responseText = WebRequestUtilities.MakeHttpGetRequestTextOnly("http://www.netflix.com/RSSFeeds", cookies);
            rssID = Regex.Match(responseText, "\"http://rss\\.netflix\\.com/QueueRSS\\?id=(?<rssID>.+?)\"").Groups["rssID"].Value;
        }

        /// <summary>
        /// Gets a list of movies that are currently in the user's queue.
        /// </summary>
        /// <returns>
        /// The list of movies currently in the user's queue.
        /// </returns>
        public Dictionary<long, Movie> GetQueue()
        {
            return GetMoviesFromRss("http://rss.netflix.com/QueueRSS?id=" + rssID);
        }

        /// <summary>
        /// Gets a list of movies that the user currently has at home.
        /// </summary>
        /// <returns>
        /// The list of movies that the user currently has at home.
        /// </returns>
        public Dictionary<long, Movie> GetMoviesAtHome()
        {
            return GetMoviesFromRss("http://rss.netflix.com/AtHomeRSS?id=" + rssID);
        }

        /// <summary>
        /// Gets a list of movies that have been recommended to the user by NetFlix.
        /// </summary>
        /// <returns>
        /// The list of movies that have been recommended to the user by NetFlix.
        /// </returns>
        public Dictionary<long, Movie> GetRecommendations()
        {
            return GetMoviesFromRss("http://rss.netflix.com/RecommendationsRSS?id=" + rssID);
        }

        /// <summary>
        /// Gets a list of movies that have recently been shipped to the user.
        /// </summary>
        /// <returns>
        /// The list of movies that have recently been shipped to the user.
        /// </returns>
        public Dictionary<long, Movie> GetRecentlyShippedMovies()
        {
            return GetMoviesFromRss("http://rss.netflix.com/TrackingRSS?id=" + rssID, "^Shipped: (?<title>.+)");
        }

        /// <summary>
        /// Gets a list of movies that the user has recently shipped back to NetFlix.
        /// </summary>
        /// <returns>
        /// The list of movies that the user has recently shipped back to NetFlix.
        /// </returns>
        public Dictionary<long, Movie> GetRecentlyReceivedMovies()
        {
            return GetMoviesFromRss("http://rss.netflix.com/TrackingRSS?id=" + rssID, "^Received: (?<title>.+)");
        }

        /// <summary>
        /// Gets a list of movies that have been newly released.
        /// </summary>
        /// <returns>
        /// A list of movies that have been newly released.
        /// </returns>
        public Dictionary<long, Movie> GetNewReleases()
        {
            return GetMoviesFromRss("http://rss.netflix.com/NewReleasesRSS");
        }

        /// <summary>
        /// Gets a list of the top 100 most-rented movies.
        /// </summary>
        /// <returns>
        /// A list of the top 100 most-rented movies.
        /// </returns>
        public Dictionary<long, Movie> GetTop100()
        {
            return GetMoviesFromRss("http://rss.netflix.com/Top100RSS");
        }

        /// <summary>
        /// Gets the information for a particular movie ID.
        /// </summary>
        /// <param name="movieID">
        /// Movie ID whose information we are to retrieve.
        /// </param>
        /// <returns>
        /// A Movie object for the given movie ID.
        /// </returns>
        public Movie GetMovie(long movieID)
        {
            JsonMovieDetails jsonDetails = (JsonMovieDetails)WebRequestUtilities.MakeJsonGetRequest("http://www.netflix.com/JSON/BobData?linkCtr=BOX&movieid=" + movieID.ToString(), typeof(JsonMovieDetails), cookies);
            return new Movie(movieID, jsonDetails.title);
        }

        /// <summary>
        /// Searches NetFlix's database for movies that match a set of search terms.
        /// </summary>
        /// <param name="searchTerms">
        /// Search terms that we are to use when querying NetFlix's database.
        /// </param>
        /// <returns>
        /// A list of movies (if any) that match the search terms.
        /// </returns>
        public Dictionary<long, Movie> FindMovies(string searchTerms)
        {
            // Make the HTTP GET request to NetFlix's search page to get the initial set of results
            string responseText = WebRequestUtilities.MakeHttpGetRequestTextOnly("http://www.netflix.com/Search?v1=" + HttpUtility.UrlEncode(searchTerms) + "&type=title&row=0&dtl=1");
            //int totalMatches = Convert.ToInt32(Regex.Match(responseText, "Movie Matches</nobr> \\((\\d+)\\)").Groups[1].Value);
            string[] values = Regex.Match(responseText, @"About[\s-0-9]*results").ToString().Split(' ');

            if (values.Length <= 1)
            {
                return new Dictionary<long, Movie>();
            }

            int totalMatches = Convert.ToInt32(values[1]);
            Dictionary<long, Movie> movies = new Dictionary<long, Movie>();
            List<Title> Movies = new List<Title>();

            if (totalMatches > MAX_TO_SEARCH)
            {
                totalMatches = MAX_TO_SEARCH;
            }

            // If there are more than 50 results, they will be separated into multiple pages, so
            // we have to make a separate HTTP request for each page
            for (int currentIndex = 0; currentIndex < totalMatches; currentIndex++)  //changed from currentIndex += 50
            {
                if (currentIndex != 0)
                    responseText = WebRequestUtilities.MakeHttpGetRequestTextOnly("http://www.netflix.com/Search?v1=" + HttpUtility.UrlEncode(searchTerms) + "&type=title&row=" + currentIndex.ToString() + "&dtl=1");

                // Extract each movie's information from the results page and add it to the return
                // list
                foreach (Match movie in Regex.Matches(responseText, "<a href=\"http://www.netflix.com/Movie/(.+?)/(?<movieID>\\d+)?(.+?)\">(?<title>[^<(]{1}.+?)</a>"))
                {
                    if (!movies.ContainsKey(Convert.ToInt64(movie.Groups["movieID"].Value)))
                    {
                        string id = movie.Groups["movieID"].Value;
                        //movies.Add(Convert.ToInt64(movie.Groups["movieID"].Value), new Movie(Convert.ToInt64(movie.Groups["movieID"].Value), movie.Groups["title"].Value));
                        JsonMovieDetails jsonDetails = (JsonMovieDetails)WebRequestUtilities.MakeJsonGetRequest("http://www.netflix.com/JSON/BobData?linkCtr=BOX&movieid=" + id, typeof(JsonMovieDetails));

                    }
                }
                    
            }

            return movies;
        }

        /// <summary>
        /// Searches NetFlix's database of "Watch Now" movies for those matching a set of search
        /// terms.
        /// </summary>
        /// <param name="searchTerms">
        /// Search terms that we are to use when querying NetFlix's database.
        /// </param>
        /// <returns>
        /// A list of watchable movies (if any) that match the search terms.
        /// </returns>
        public Dictionary<long, Movie> FindWatchableMovies(string searchTerms)
        {
            // Make the HTTP GET request to NetFlix's search page
            string responseText = WebRequestUtilities.MakeHttpGetRequestTextOnly("http://www.netflix.com/WiSearch?v1=" + HttpUtility.UrlEncode(searchTerms), cookies);
            string[] values = Regex.Match(responseText, @"About[\s-0-9]*results").ToString().Split(' ');

            if (values.Length <= 1)
            {
                return new Dictionary<long, Movie>();
            }

            Dictionary<long, Movie> movies = new Dictionary<long, Movie>();

            // Extract each movie's information from the results page and add it to the return list
            foreach (Match movie in Regex.Matches(responseText, "<a href=\"http://www.netflix.com/Movie/(.+?)/(?<movieID>\\d+)?(.+?)\">(?<title>[^<(]{1}.+?)</a>"))
            {
                if (!movies.ContainsKey(Convert.ToInt64(movie.Groups["movieID"].Value)))
                {
                    movies.Add(Convert.ToInt64(movie.Groups["movieID"].Value), new Movie(Convert.ToInt64(movie.Groups["movieID"].Value), movie.Groups["title"].Value));
                }
            }

            return movies;
        }

        /// <summary>
        /// Gets a list of genres that are present on NetFlix's site.
        /// </summary>
        /// <returns>
        /// A list of genres on NetFlix's site.
        /// </returns>
        //public List<Genre> GetGenres()
        //{
        //    // Make the HTTP GET request to the genres page
        //    string responseText = WebRequestUtilities.MakeHttpGetRequestTextOnly("http://www.netflix.com/AllGenres");
        //    List<Genre> genres = new List<Genre>();

        //    // Create each Genre object, based on whether it has a parent genre or not
        //    //foreach (Match genre in Regex.Matches(responseText, "<a href='http://www.netflix.com/(Sub)*Genre\\?sgid=(?<genreID>\\d+)(&pgid=(?<parentGenreID>\\d+))*'>(?<genreName>.+?)</a>"))
        //    foreach (Match genre in Regex.Matches(responseText, "<a href='http://www.netflix.com/SubGenre/.*/(?<parentGenreID>\\d+)/.*/(?<genreID>\\d+)>(?<genreName>.*)</a>"))
        //    {
        //        if (genre.Groups["parentGenreID"].Success)
        //            genres.Add(new Genre(Convert.ToInt32(genre.Groups["genreID"].Value), Convert.ToInt32(genre.Groups["parentGenreID"].Value), genre.Groups["genreName"].Value));

        //        else
        //            genres.Add(new Genre(Convert.ToInt32(genre.Groups["genreID"].Value), genre.Groups["genreName"].Value));
        //    }

        //    return genres;
        //}

        /*public Dictionary<long, Movie> BrowseGenre(Genre genre, uint pageNumber, ref bool nextPageExists)
        {
            string responseText = WebRequestUtilities.MakeHttpGetRequestTextOnly("http://www.netflix.com/Genre?sgid=" + genre.ID.ToString() + "&pageNum=" + pageNumber + (genre.ParentID != 0 ? "&pgid=" + genre.ParentID.ToString() : ""));
            responseText = responseText.Substring(responseText.IndexOf("<div class='inactive'>Sort By</div>"));
            Dictionary<long, Movie> movies = new Dictionary<long, Movie>();

            foreach (Match movie in Regex.Matches(responseText, "<a href=\"http://www.netflix.com/Movie/(.+?)/(?<movieID>\\d+)\">(?<title>[^<(]{1}.+?)</a>"))
                movies.Add(Convert.ToInt64(movie.Groups["movieID"].Value), new Movie(Convert.ToInt64(movie.Groups["movieID"].Value), movie.Groups["title"].Value));

            nextPageExists = (responseText.Contains(".nflximg.com/us/pages/next_s0_v2.gif\"") || responseText.Contains(">More " + genre.Name + " ><"));

            return movies;
        }*/

        /// <summary>
        /// Adds the specified movie ID to the user's queue.
        /// </summary>
        /// <param name="movieID">
        /// ID of the movie that we are to add to the queue.
        /// </param>
        public QueueResult AddToQueue(long movieID)
        {
            string url = "http://www.netflix.com/AddToQueue?movieid=" + movieID;

            HttpWebResponse webResponse = WebRequestUtilities.MakeHttpGetRequest(url, cookies);
            
            //string response = WebRequestUtilities.MakeHttpGetRequestTextOnly("http://www.netflix.com/QueueAddConfirmation?action=ADD&movieid=" +
            //                                       movieID +
            //                                       "&result=qtype%253DDVD%2526code%253D0&result=qtype%253DED%2526code%253D-4", cookies);

            string response = WebRequestUtilities.MakeHttpGetRequestTextOnly(webResponse.ResponseUri.AbsoluteUri, cookies);

            if (response.ToUpper().Contains("HAS BEEN ADDED TO YOUR"))
            {
                return QueueResult.Success;
            }
            
            return response.ToUpper().Contains("THIS MOVIE IS ALREADY IN YOUR") ? QueueResult.AlreadyInQueue : QueueResult.Failed;
        }

        /// <summary>
        /// Movies the specified movie ID to a different location in the user's queue.
        /// </summary>
        /// <param name="movieID">
        /// ID of the movie that we are to move.
        /// </param>
        /// <param name="newPosition">
        /// New position in the queue that the movie is to occupy.
        /// </param>
        public void ReorderQueue(long movieID, int newPosition)
        {
            // Make the JSON call to reorder the user's queue
            JsonQueueReorderResponse response = (JsonQueueReorderResponse)WebRequestUtilities.MakeJsonGetRequest("http://www.netflix.com/QueueReorder?movieid=" + movieID.ToString() + "&pos=" + newPosition.ToString(), typeof(JsonQueueReorderResponse), cookies);

            // Check to make sure that the operation was successful
            if (!response.result)
                throw new QueueException("Unable to reorder the queue.");
        }

        /// <summary>
        /// Removes a specified movie from the user's queue.
        /// </summary>
        /// <param name="movieID">
        /// ID of the movie that we are to remove.
        /// </param>
        public void DeleteFromQueue(long movieID)
        {
            // Make the HTTP POST call to remove the movie from the queue
            Dictionary<string, string> postFields = new Dictionary<string, string>();
            postFields.Add("updateQueueBtn", "updateQueueBtn");
            postFields.Add("R" + movieID.ToString(), "on");

            string responseText = WebRequestUtilities.MakeHttpPostRequestTextOnly("http://www.netflix.com/Queue", postFields, cookies);
        }

        /// <summary>
        /// Helper method that extracts movie information from RSS feeds.
        /// </summary>
        /// <param name="rssURL">
        /// URL from which we are to retrieve the feed data.
        /// </param>
        /// <returns>
        /// A list of movies that were in the RSS data.
        /// </returns>
        protected Dictionary<long, Movie> GetMoviesFromRss(string rssURL)
        {
            return GetMoviesFromRss(rssURL, "(\\d+\\- )?(?<title>.+)");
        }

        /// <summary>
        /// Helper method that extracts movie information from RSS feeds, allowing the inclusion
        /// of a regular expression to be applied against the title to determine which movies are 
        /// to be brought back.
        /// </summary>
        /// <param name="rssURL">
        /// URL from which we are to retrieve the feed data.
        /// </param>
        /// <param name="matchRegex">
        /// Regular expression to be applied against the title.
        /// </param>
        /// <returns>
        /// A list of matching movies that were in the RSS data.
        /// </returns>
        protected Dictionary<long, Movie> GetMoviesFromRss(string rssURL, string matchRegex)
        {
            // Get the RSS XML from the URL
            string responseText = WebRequestUtilities.MakeHttpGetRequestTextOnly(rssURL);
            XmlDocument rssXML = new XmlDocument();
            Dictionary<long, Movie> movies = new Dictionary<long, Movie>();

            rssXML.LoadXml(responseText);

            XmlNodeList queueItems = rssXML.SelectNodes("/rss/channel/item");

            foreach (XmlNode queueItem in queueItems)
            {
                string title = Regex.Match(queueItem.SelectSingleNode("title").InnerText, matchRegex).Groups["title"].Value;

                // If the movie's title doesn't match the regular expression, don't add it to the
                // return list
                if (String.IsNullOrEmpty(title))
                    continue;

                // Otherwise, create the movie object and add it to the list
                long movieID = Convert.ToInt64(Regex.Match(queueItem.SelectSingleNode("link").InnerText, "http://www.netflix.com/Movie/.+?/(?<id>\\d+)").Groups["id"].Value);

                movies.Add(movieID, new Movie(movieID, title));
            }

            return movies;
        }

        /// <summary>
        /// Explicitly logs the current session out from NetFlix's site.
        /// </summary>
        public void Dispose()
        {
            WebRequestUtilities.MakeHttpGetRequestTextOnly("http://www.netflix.com/LogoutPage", cookies);
        }
    }
}